package han.chensing.latos.common.file;

import han.chensing.latos.common.file.annotation.CirnoFindParent;
import han.chensing.latos.common.file.annotation.CirnoNotSerialize;
import org.json.JSONArray;
import org.json.JSONObject;
import org.luaj.vm2.*;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;

@SuppressWarnings({"unused", "SameParameterValue"})
public class CirnoSerialize {

    private static final String JAVA_ARRAY="JAVA_ARRAY";
    private static final String JAVA_LIST="JAVA_LIST";
    private static final String JAVA_CLASS="JAVA_CLASS";
    private static final String JAVA_ENUM="JAVA_ENUM";

    private Object constructAnObjectByClass(Class<?> aClass)
            throws InvocationTargetException, InstantiationException,
            IllegalAccessException {
        Constructor<?> constructor;
        try {
            constructor = aClass.getConstructor();
        } catch (NoSuchMethodException e) {
            System.err.println("No non-args constructor found in class "+aClass.getName());
            return null;
        }
        return constructor.newInstance();
    }

    public Object luaTableToObject(LuaTable luaTable)
            throws NoSuchFieldException, InvocationTargetException,
            InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException {
        String aClass = luaTable.get(LuaString.valueOf(JAVA_CLASS)).tojstring();
        Class<?> clazz = Class.forName(aClass);
        Object o= constructAnObjectByClass(clazz);
        if (o==null)return null;
        for (LuaValue key : luaTable.keys()) {
            LuaValue luaValue = luaTable.get(key);
            String name = key.tojstring();
            Field declaredField = findFieldInClass(clazz, name);
            if (declaredField==null)continue;
            if (isNotSerializable(declaredField))continue;
            Object o1 = luaValueToSingleObject(luaValue);
            declaredField.setAccessible(true);
            declaredField.set(o,o1);
            declaredField.setAccessible(false);
        }
        return o;
    }

    private Object luaValueToSingleObject(LuaValue luaValue)
            throws NoSuchFieldException, ClassNotFoundException,
            InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
        if (luaValue==null)return null;
        if (luaValue.equals(LuaValue.NIL))return null;
        if (luaValue.isint()) {
            return luaValue.toint();
        }else if (luaValue.isnumber()){
            return (float)luaValue.checkdouble();
        }else if (luaValue.isboolean()){
            return luaValue.toboolean();
        }else if (luaValue.isstring()){
            String string = luaValue.tojstring();
            if (string.contains(JAVA_ENUM))
                return luaValueToEnum(string);
            return string;
        }else if (luaValue.isnil()){
            return null;
        }
        LuaTable table = luaValue.checktable();
        if (!table.get(LuaString.valueOf(JAVA_LIST)).equals(LuaValue.NIL)){
            return luaValueToList(luaValue);
        }else if (!table.get(LuaString.valueOf(JAVA_ARRAY)).equals(LuaValue.NIL)){
            LuaValue typeClass = table.get(LuaString.valueOf(JAVA_ARRAY));
            String className = typeClass.tojstring();
            Class<?> clazz=Class.forName(className);
            return luaValueToArray(luaValue, clazz);
        }
        return luaTableToObject(table);
    }

    private Object luaValueToEnum(String string)
            throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        String[] strings = string.split(JAVA_ENUM);
        Class<?> enumClass = Class.forName(strings[0]);
        Method valueOfMethod = enumClass.getDeclaredMethod("valueOf", String.class);
        return valueOfMethod.invoke(null, strings[1]);
    }

    private Object[] luaValueToArray(LuaValue luaValue,Class<?> arrayTypeClazz)
            throws NoSuchFieldException, ClassNotFoundException,
            InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
        Class<?> componentType = arrayTypeClazz.getComponentType();
        List<Object> objectsList=luaValueToList(luaValue);
        if (objectsList==null)return null;
        Object[] objects=new Object[objectsList.size()];
        objectsList.toArray(objects);
        Object[] outObjects = (Object[]) Array.newInstance(componentType, objects.length);
        for (int i = 0; i < objects.length; i++) {
            outObjects[i]=componentType.cast(objects[i]);
        }
        return outObjects;
    }

    private List<Object> luaValueToList(LuaValue luaValue)
            throws NoSuchFieldException, ClassNotFoundException,
            InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
        if (!luaValue.istable())return null;
        LuaTable table = luaValue.checktable();
        List<Object> objects=new ArrayList<>();
        LuaValue[] keys = table.keys();
        for (LuaValue key : keys) {
            if (key.tojstring().equals(JAVA_LIST)||
                    key.tojstring().equals(JAVA_ARRAY))continue;
            LuaValue value = table.get(key);
            objects.add(luaValueToSingleObject(value));
        }
        return objects;
    }

    public JSONObject serializeToJson(Object sourceObject) throws IllegalAccessException {
        JSONObject jsonObject=new JSONObject();
        serializeToJson(jsonObject,sourceObject);
        return jsonObject;
    }

    public Object jsonToObject(JSONObject jsonObject)
            throws ClassNotFoundException, InvocationTargetException,
            InstantiationException, IllegalAccessException, NoSuchMethodException,
            NoSuchFieldException {
        if (jsonObject.has(JAVA_ENUM)) {
            return jsonObjectToEnum(jsonObject);
        }
        String javaClass = (String) jsonObject.get(JAVA_CLASS);
        Class<?> aClass = Class.forName(javaClass);
        Object o = constructAnObjectByClass(aClass);
        if (o==null)return null;
        Set<String> strings=jsonObject.keySet();
        for (String key : strings) {
            if (key.equals(JAVA_CLASS))continue;
            Object value = jsonObject.get(key);
            Field declaredField = findFieldInClass(aClass, key);
            if (declaredField==null)continue;
            if (isNotSerializable(declaredField))continue;
            Object object = jsonObjectToSingleObject(value);
            declaredField.setAccessible(true);
            declaredField.set(o,typeSafeCast(declaredField.getType(),object));
            declaredField.setAccessible(false);
        }
        return o;
    }

    private Object jsonObjectToEnum(JSONObject jsonObject)
            throws InvocationTargetException,
            IllegalAccessException, NoSuchMethodException, ClassNotFoundException {
        String className = jsonObject.get(JAVA_ENUM).toString();
        Class<?> aClass = Class.forName(className);
        Method declaredMethod = aClass.getDeclaredMethod("valueOf", String.class);
        return declaredMethod.invoke(null, jsonObject.get("value").toString());
    }

    private Object jsonObjectToSingleObject(Object o)
            throws ClassNotFoundException, InvocationTargetException,
            InstantiationException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException {
        if (o==null)return null;
        if (
                o instanceof Integer||
                        o instanceof Boolean||
                        o instanceof Float||
                        o instanceof Double||
                        o instanceof Long||
                        o instanceof Short||
                        o instanceof String){
            if (o instanceof Double)return (float)(double)o;
            return o;
        }else if (o instanceof JSONObject){
            return jsonToObject((JSONObject) o);
        }else if (o.getClass().equals(JSONArray.class)){
            return jsonObjectToArrayOrList((JSONArray) o);
        }else if (o instanceof BigDecimal){
            return ((BigDecimal)o).floatValue();
        }
        return null;
    }

    private Object jsonObjectToArrayOrList(JSONArray array)
            throws ClassNotFoundException, InvocationTargetException,
            InstantiationException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException {
        int length = array.length();
        if (length<1)return null;
        String string = array.get(0).toString();
        if (string.startsWith(JAVA_ARRAY)){
            return jsonObjectToArray(array,string.substring(JAVA_ARRAY.length()+1));
        }else if (string.startsWith(JAVA_LIST)){
            return jsonObjectToList(array);
        }
        return null;
    }

    private Object jsonObjectToList(JSONArray array)
            throws ClassNotFoundException, InvocationTargetException,
            InstantiationException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException {
        int length = array.length();
        List<Object> objects=new ArrayList<>();
        for (int i = 1; i < length; i++) {
            objects.add(jsonObjectToSingleObject(array.get(i)));
        }
        return objects;
    }

    private Object jsonObjectToArray(JSONArray array, String className)
            throws ClassNotFoundException, InvocationTargetException,
            InstantiationException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException {
        int length = array.length();
        Class<?> aClass = Class.forName(className);
        Object[] objects = (Object[]) Array.newInstance(aClass, length - 1);
        for (int i = 1; i < length; i++) {
            Object o1 = array.get(i);
            objects[i-1]=aClass.cast(jsonObjectToSingleObject(o1));
        }
        return objects;
    }

    private void serializeToJson(JSONObject parentObject,Object sourceObject) throws IllegalAccessException {
        Class<?> aClass = sourceObject.getClass();
        Field[] declaredFields = findDeclaredFields(aClass);
        parentObject.put(JAVA_CLASS,sourceObject.getClass().getName());
        for (Field declaredField : declaredFields) {
            if (isNotSerializable(declaredField))continue;
            declaredField.setAccessible(true);
            Object o = declaredField.get(sourceObject);
            declaredField.setAccessible(false);
            parentObject.put(
                    declaredField.getName(),
                    serializeSingleObjectToJsonObject(o)
            );
        }
    }

    private Object serializeSingleObjectToJsonObject(Object object) throws IllegalAccessException {
        if (object==null)return null;
        Class<?> aClass = object.getClass();
        if (
                aClass.equals(String.class)) {
            return object;
        } else if (
                aClass.equals(Integer.class) ||
                        aClass.equals(Double.class) ||
                        aClass.equals(Float.class) ||
                        aClass.equals(Long.class) ||
                        aClass.equals(Short.class)) {
            return object;
        }else if (aClass.equals(Boolean.class)){
            return Boolean.parseBoolean(String.valueOf(object));
        } else if (aClass.isEnum()) {
            JSONObject jsonObject=new JSONObject();
            jsonObject.put(JAVA_ENUM,aClass.getName());
            jsonObject.put("value",((Enum<?>)object).name());
            return jsonObject;
        } else if (
                aClass.isArray()) {
            return convertArrayToJsonObject(object,aClass.getComponentType());
        } else if (
                object instanceof List) {
            return convertListToJsonObject(object);
        }
        JSONObject jsonObject=new JSONObject();
        serializeToJson(jsonObject, object);
        return jsonObject;
    }

    private Object convertListToJsonObject(Object object) throws IllegalAccessException{
        List<?> list=(List<?>) object;
        Object[] objects=new Object[list.size()];
        list.toArray(objects);
        Object[] array = (Object[]) convertArrayToJsonObject(objects,null);
        array[0]=JAVA_LIST;
        return array;
    }

    private Object convertArrayToJsonObject(Object object,Class<?> arrayClass) throws IllegalAccessException {
        Object[] objects=(Object[]) object;
        Object[] outObjects=new Object[objects.length+1];
        if (arrayClass!=null)
            outObjects[0]=JAVA_ARRAY+":"+arrayClass.getName();
        for (int i = 1; i < outObjects.length; i++) {
            Object o = objects[i-1];
            Object o1 = serializeSingleObjectToJsonObject(o);
            outObjects[i]=o1;
        }
        return outObjects;
    }

    public LuaTable serializeToLuaTable(Object sourceObject) throws IllegalAccessException {
        LuaTable luaTable=new LuaTable();
        serializeToLuaTable(luaTable,sourceObject);
        return luaTable;
    }

    private boolean isNotSerializable(Field declaredField){
        return Modifier.isStatic(declaredField.getModifiers()) ||
                fieldAnnotationExists(declaredField,CirnoNotSerialize.class)||
                declaredField.getName().contains("$");
    }

    private boolean fieldAnnotationExists(Field field,Class<?> annotationClass){
        Annotation[] declaredAnnotations = field.getDeclaredAnnotations();
        for (Annotation declaredAnnotation : declaredAnnotations) {
            if (declaredAnnotation.annotationType().equals(annotationClass))
                return true;
        }
        return false;
    }

    private boolean classAnnotationExists(Class<?> clazz,Class<?> annotationClass){
        Annotation[] declaredAnnotations = clazz.getDeclaredAnnotations();
        for (Annotation declaredAnnotation : declaredAnnotations) {
            if (declaredAnnotation.annotationType().equals(annotationClass))
                return true;
        }
        return false;
    }

    private void serializeToLuaTable(LuaTable parentTable, Object sourceObject) throws IllegalAccessException {
        Class<?> aClass = sourceObject.getClass();
        Field[] declaredFields = findDeclaredFields(aClass);
        parentTable.set(
                LuaString.valueOf(JAVA_CLASS),
                LuaString.valueOf(aClass.getName())
        );
        for (Field declaredField : declaredFields) {
            if (isNotSerializable(declaredField))continue;
            declaredField.setAccessible(true);
            Object o = declaredField.get(sourceObject);
            declaredField.setAccessible(false);
            LuaValue luaValue = serializeSingleObjectToLuaValue(o);
            parentTable.set(
                    LuaString.valueOf(declaredField.getName()),
                    luaValue
            );
        }
    }

    private LuaValue serializeSingleObjectToLuaValue(Object object) throws IllegalAccessException {
        if (object==null)return LuaValue.NIL;
        Class<?> aClass = object.getClass();
        if (
                aClass.equals(String.class)) {
            return LuaString.valueOf((String) object);
        } else if (isPackedNumberClass(aClass)) {
            return LuaNumber.valueOf(String.valueOf(object)).tonumber();
        }else if (aClass.equals(Boolean.class)){
            return LuaBoolean.valueOf(Boolean.parseBoolean(String.valueOf(object)));
        } else if (aClass.isEnum()) {
            return LuaString.valueOf(aClass.getName()+JAVA_ENUM+((Enum<?>)object).name());
        } else if (
                aClass.isArray()) {
            return convertArrayToLuaTable(object);
        } else if (
                object instanceof List) {
            return convertListToLuaTable(object);
        }
        LuaTable luaTable = new LuaTable();
        serializeToLuaTable(luaTable, object);
        return luaTable;
    }

    private LuaTable convertArrayToLuaTable(Object o) throws IllegalAccessException {
        Object[] array=(Object[]) o;
        LuaTable luaTable=new LuaTable();
        luaTable.set(
                LuaString.valueOf(JAVA_ARRAY),
                LuaString.valueOf(array.getClass().getName())
        );
        for (int i = 0,j=1; i < array.length; i++,j++) {
            luaTable.set(j, serializeSingleObjectToLuaValue(array[i]));
        }
        return luaTable;
    }

    private LuaTable convertListToLuaTable(Object o) throws IllegalAccessException {
        List<?> list=(List<?>) o;
        LuaTable luaTable=new LuaTable();
        luaTable.set(
                LuaString.valueOf(JAVA_LIST),
                LuaString.valueOf(JAVA_LIST)
        );
        for (int i = 0,j=1; i < list.size(); i++,j++) {
            luaTable.set(j, serializeSingleObjectToLuaValue(list.get(i)));
        }
        return luaTable;
    }

    private boolean isPackedNumberClass(Class<?> aClass){
        return aClass!=null&&(aClass.equals(Integer.class) ||
                aClass.equals(Double.class) ||
                aClass.equals(Float.class) ||
                aClass.equals(Long.class) ||
                aClass.equals(Short.class));
    }

    private boolean isNormalNumberClass(Class<?> aClass){
        return aClass!=null&&(aClass.equals(int.class) ||
                aClass.equals(double.class) ||
                aClass.equals(float.class) ||
                aClass.equals(long.class) ||
                aClass.equals(short.class));
    }

    private Class<?> normalClassToPackedClass(Class<?> normalClass) throws ClassNotFoundException {
        String s = castFirstLetterToCapital(normalClass.getSimpleName());
        if (s.equals("Int"))s="Integer";
        return Class.forName("java.lang."+s);
    }

    private Class<?> packedClassToNormalClass(Class<?> packedClass) throws IllegalAccessException, NoSuchFieldException {
        Field typeField = packedClass.getDeclaredField("TYPE");
        return (Class<?>) typeField.get(null);
    }

    private Object parseStringToNormalObject(String s, Class<?> clazz)
            throws ClassNotFoundException, InvocationTargetException,
            IllegalAccessException, NoSuchMethodException {
        if (isNormalNumberClass(clazz))clazz=normalClassToPackedClass(clazz);
        String simpleName = clazz.getSimpleName();
        if (simpleName.equals("Integer"))simpleName="Int";
        Method parseMethod = clazz.getDeclaredMethod("parse" + simpleName,String.class);
        return parseMethod.invoke(null, s);
    }

    private String castFirstLetterToCapital(String s){
        StringBuilder stringBuilder = new StringBuilder(s);
        stringBuilder.setCharAt(0,Character.toUpperCase(stringBuilder.charAt(0)));
        return stringBuilder.toString();
    }

    private Field[] findDeclaredFields(Class<?> clazz){
        List<Field> fields=new ArrayList<>();
        findDeclaredFields(clazz,fields);
        Field[] fieldsArray=new Field[fields.size()];
        fields.toArray(fieldsArray);
        return fieldsArray;
    }

    private Field findFieldInClass(Class<?> clazz,String name){
        Field[] declaredFields = findDeclaredFields(clazz);
        for (Field declaredField : declaredFields) {
            if(declaredField.getName().equals(name))return declaredField;
        }
        return null;
    }

    private void findDeclaredFields(Class<?> clazz,List<Field> fields){
        Field[] declaredFields = clazz.getDeclaredFields();
        fields.addAll(Arrays.asList(declaredFields));
        if (classAnnotationExists(clazz,CirnoFindParent.class)) {
            findDeclaredFields(clazz.getSuperclass(),fields);
        }
    }

    private Object typeSafeCast(Class<?> clazz,Object object)
            throws ClassNotFoundException, InvocationTargetException,
            IllegalAccessException, NoSuchMethodException, NoSuchFieldException,
            InstantiationException {
        if (!(isPackedNumberClass(clazz)|| isNormalNumberClass(clazz))) return object;
        String string = object.toString();
        Class<?> targetClass = object.getClass();
        Object normalClassNumber=parseStringToNormalObject(string,targetClass);
        if (isPackedNumberClass(targetClass)){
            return targetClass.getConstructor(
                    packedClassToNormalClass(targetClass)).newInstance(normalClassNumber);
        }
        return normalClassNumber;
    }

}
